<?php
namespace common\modules\storage\filesystem\adapter;

use ArrayIterator;
use Qiniu\Storage\UploadManager;
use Qiniu\Auth;
use League\Flysystem\Adapter\AbstractAdapter;
use League\Flysystem\AdapterInterface;
use League\Flysystem\Config;
use League\Flysystem\Util;
use RecursiveArrayIterator;
use RecursiveIteratorIterator;
use Qiniu\Storage\BucketManager;
use Qiniu\Storage\ResumeUploader;

/**
 * QiniuAdapter
 * @author Xuegui Yuan <yuanxuegui@163.com>
 */
class QiniuAdapter extends AbstractAdapter
{

    /**
     *
     * @var array
     */
    protected static $resultMap = [
        'Body' => 'contents',
        'fsize' => 'size',
        'mimeType' => 'mimetype'
    ];

    /**
     *
     * @var array
     */
    protected static $metaOptions = [
        'CacheControl',
        'Expires',
        'StorageClass',
        'ServerSideEncryption',
        'Metadata',
        'ACL',
        'ContentType',
        'ContentEncoding'
    ];

    /**
     *
     * @var BucketManager $bucketManager
     */
    protected $bucketManager;

    /**
     *
     * @var UploadManager $uploadManager
     */
    protected $uploadManager;

    /**
     *
     * @var Auth $auth
     */
    protected $auth;

    /**
     *
     * @var string $bucketName
     */
    protected $bucketName;

    /**
     *
     * @var string $uploadToken
     */
    protected $uploadToken;

    /**
     *
     * @var array $options
     */
    protected $options = [];

    /**
     * Constructor.
     *
     * @param string $accessKey            
     * @param string $secretKey            
     * @param string $bucketName            
     * @param array $options            
     */
    public function __construct($accessKey, $secretKey, $bucketName, $prefix = '', array $options = [])
    {
        $this->auth = new Auth($accessKey, $secretKey);
        $this->bucketName = $bucketName;
        $this->bucketManager = new BucketManager($this->auth);
        $this->uploadManager = new UploadManager();
        $this->uploadToken = $this->auth->uploadToken($bucketName);
        $this->setPathPrefix($prefix);
        $this->options = $options;
    }

    /**
     * Get the UploadManager instance.
     *
     * @return UploadManager
     */
    public function getUploadManager()
    {
        return $this->uploadManager;
    }

    /**
     * Get the Auth $auth.
     *
     * @return string
     */
    public function getAuth()
    {
        return $this->auth;
    }

    /**
     * Get the upload token.
     *
     * @return string
     */
    public function getUploadToken()
    {
        return $this->uploadToken;
    }

    /**
     * Write a new file.
     *
     * @param string $path            
     * @param string $contents            
     * @param Config $config
     *            Config object
     *            
     * @return false|array false on failure file meta data on success
     */
    public function write($path, $contents, Config $config)
    {
        return $this->upload($path, $contents, $config);
    }

    /**
     * Update a file.
     *
     * @param string $path            
     * @param string $contents            
     * @param Config $config
     *            Config object
     *            
     * @return false|array false on failure file meta data on success
     */
    public function update($path, $contents, Config $config)
    {
        return $this->upload($path, $contents, $config);
    }

    /**
     * Rename a file.
     *
     * @param string $path            
     * @param string $newpath            
     *
     * @return bool
     */
    public function rename($path, $newpath)
    {
        $oldname = $this->applyPathPrefix($path);
        $newname = $this->applyPathPrefix($newpath);
        
        if ($this->bucketManager->move($this->$bucket, $oldname, $this->$bucket, $newname)) {
            return false;
        }
        
        return true;
    }

    /**
     * Delete a file.
     *
     * @param string $path            
     *
     * @return bool
     */
    public function delete($path)
    {
        $location = $this->applyPathPrefix($path);
        
        return $this->bucketManager->delete($this->bucketName, $location) == null;
    }

    /**
     * Delete a directory.
     *
     * @param string $dirname            
     *
     * @return bool
     */
    public function deleteDir($dirname)
    {
        return true;
    }

    /**
     * Create a directory.
     *
     * @param string $dirname
     *            directory name
     * @param Config $config            
     *
     * @return bool|array
     */
    public function createDir($dirname, Config $config)
    {
        return $this->upload($dirname . '/', '', $config);
    }

    /**
     * Check whether a file exists.
     *
     * @param string $path            
     *
     * @return bool
     */
    public function has($path)
    {
        $location = $this->applyPathPrefix($path);
        $result = $this->bucketManager->stat($this->bucketName, $location);
        return $result[0] != null;
    }

    /**
     * Read a file.
     *
     * @param string $path            
     *
     * @return false|array
     */
    public function read($path)
    {
        $response = $this->readObject($path);
        
        if ($response !== false) {
            $response['contents'] = $response['contents']->getContents();
        }
        
        return $response;
    }

    /**
     * List contents of a directory.
     *
     * @param string $directory            
     * @param bool $recursive            
     *
     * @return array
     */
    public function listContents($directory = '', $recursive = false)
    {
        $prefix = $this->applyPathPrefix(rtrim($directory, '/') . '/');
        $options = [
            'Bucket' => $this->bucket,
            'Prefix' => ltrim($prefix, '/')
        ];
        
        if ($recursive === false) {
            $options['Delimiter'] = '/';
        }
        
        $listing = $this->retrievePaginatedListing($options);
        $normalizer = [
            $this,
            'normalizeResponse'
        ];
        $normalized = array_map($normalizer, $listing);
        
        return Util::emulateDirectories($normalized);
    }

    /**
     *
     * @param array $options            
     *
     * @return array
     */
    protected function retrievePaginatedListing(array $options)
    {
        $resultPaginator = $this->bucketManager->listFiles($this->bucketName, $options['Prefix'], $options['Marker'], $options['Limit'], $options['Delimiter']);
        $listing = [];
        
        foreach ($resultPaginator as $result) {
            $listing = array_merge($listing, $result->get('key') ?  : [], $result->get('fsize') ?  : []);
        }
        
        return $listing;
    }

    /**
     * Get all the meta data of a file or directory.
     *
     * @param string $path            
     *
     * @return false|array
     */
    public function getMetadata($path)
    {
        $key = $this->applyPathPrefix($path);
        $result = $this->bucketManager->stat($this->bucketName, $key);
        return $this->normalizeResponse($result[0], $path);
    }

    /**
     * Get all the meta data of a file or directory.
     *
     * @param string $path            
     *
     * @return false|array
     */
    public function getSize($path)
    {
        return $this->getMetadata($path);
    }

    /**
     * Get the mimetype of a file.
     *
     * @param string $path            
     *
     * @return false|array
     */
    public function getMimetype($path)
    {
        return $this->getMetadata($path);
    }

    /**
     * Get the timestamp of a file.
     *
     * @param string $path            
     *
     * @return false|array
     */
    public function getTimestamp($path)
    {
        return $this->getMetadata($path);
    }

    /**
     * Write a new file using a stream.
     *
     * @param string $path            
     * @param resource $resource            
     * @param Config $config
     *            Config object
     *            
     * @return array|false false on failure file meta data on success
     */
    public function writeStream($path, $resource, Config $config)
    {
        $stat = fstat($resource);
        $size = $stat['size'];
        if ($size <= \Qiniu\Config::BLOCK_SIZE) {
            $data = fread($resource, $size);
            if ($data === false) {
                throw new \Exception("file can not read", 1);
            }
            return $this->upload($path, $data, $config);
        }
        else {
            $key = $this->applyPathPrefix($path);
            $options = $this->getOptionsFromConfig($config);
            $acl = isset($options['ACL']) ? $options['ACL'] : 'private';
            
            if (! isset($options['ContentType'])) {
                $options['ContentType'] = 'application/octet-stream';
            }
            
            if (! isset($options['ContentLength'])) {
                $options['ContentLength'] = $size;
            }
            $up = new ResumeUploader(
                $this->uploadToken,
                $key,
                $resource,
                $size,
                null,
                'application/octet-stream',
                new \Qiniu\Config()
            );
            $up->upload();
            return $this->normalizeResponse($options, $key);
        }
    }

    /**
     * Update a file using a stream.
     *
     * @param string $path            
     * @param resource $resource            
     * @param Config $config
     *            Config object
     *            
     * @return array|false false on failure file meta data on success
     */
    public function updateStream($path, $resource, Config $config)
    {
        $stat = fstat($resource);
        $size = $stat['size'];
        if ($size <= \Qiniu\Config::BLOCK_SIZE) {
            $data = fread($resource, $size);
            if ($data === false) {
                throw new \Exception("file can not read", 1);
            }
            return $this->upload($path, $data, $config);
        }
    }

    /**
     * Copy a file.
     *
     * @param string $path            
     * @param string $newpath            
     *
     * @return bool
     */
    public function copy($path, $newpath)
    {
        $visibility = $this->getRawVisibility($path);
        $from_key = $this->applyPathPrefix($path);
        $to_key = $this->applyPathPrefix($newpath);
        
        return $this->bucketManager->copy($this->$bucketName, $from_key, $this->$bucketName, $to_key) == null;
    }

    /**
     * Read a file as a stream.
     *
     * @param string $path            
     *
     * @return array|false
     */
    public function readStream($path)
    {
        $response = $this->readObject($path);
        
        if ($response !== false) {
            $response['stream'] = $response['contents']->detach();
            rewind($response['stream']);
            unset($response['contents']);
        }
        
        return $response;
    }

    /**
     * Read an object and normalize the response.
     *
     * @param
     *            $path
     *            
     * @return array|bool
     */
    protected function readObject($path)
    {
        return false;
    }

    /**
     * Set the visibility for a file.
     *
     * @param string $path            
     * @param string $visibility            
     *
     * @return array|false file meta data
     */
    public function setVisibility($path, $visibility)
    {
        return false;
    }

    /**
     * Get the visibility of a file.
     *
     * @param string $path            
     *
     * @return array|false
     */
    public function getVisibility($path)
    {
        return [
            'visibility' => $this->getRawVisibility($path)
        ];
    }

    /**
     *
     * @ERROR!!!
     *
     */
    public function applyPathPrefix($prefix)
    {
        return ltrim(parent::applyPathPrefix($prefix), '/');
    }

    /**
     *
     * @ERROR!!!
     *
     */
    public function setPathPrefix($prefix)
    {
        $prefix = ltrim($prefix, '/');
        
        return parent::setPathPrefix($prefix);
    }

    /**
     * Get the object acl presented as a visibility.
     *
     * @param string $path            
     *
     * @return string
     */
    protected function getRawVisibility($path)
    {
        return false;
    }

    /**
     * Upload an object.
     *
     * @param
     *            $path
     * @param
     *            $body
     * @param Config $config            
     *
     * @return array
     */
    protected function upload($path, $body, Config $config)
    {
        $key = $this->applyPathPrefix($path);
        $options = $this->getOptionsFromConfig($config);
        $acl = isset($options['ACL']) ? $options['ACL'] : 'private';
        
        if (! isset($options['ContentType']) && is_string($body)) {
            $options['ContentType'] = Util::guessMimeType($path, $body);
        }
        
        if (! isset($options['ContentLength'])) {
            $options['ContentLength'] = is_string($body) ? Util::contentSize($body) : Util::getStreamSize($body);
        }
        
        $this->uploadManager->put($this->uploadToken, $key, $body);
        
        return $this->normalizeResponse($options, $key);
    }

    /**
     * Upload an file.
     *
     * @param
     *            $path
     * @param
     *            $body
     * @param Config $config            
     *
     * @return array
     */
    protected function uploadFile($path, $filePath, Config $config)
    {
        $key = $this->applyPathPrefix($path);
        $options = $this->getOptionsFromConfig($config);
        $acl = isset($options['ACL']) ? $options['ACL'] : 'private';
        
        $this->uploadManager->putFile($this->uploadToken, $key, $filePath);
        
        return $this->normalizeResponse($options, $key);
    }

    /**
     * Get options from the config.
     *
     * @param Config $config            
     *
     * @return array
     */
    protected function getOptionsFromConfig(Config $config)
    {
        $options = $this->options;
        $visibility = $config->get('visibility');
        if ($visibility) {
            // For local reference
            $options['visibility'] = $visibility;
            // For external reference
            $options['ACL'] = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private';
        }
        $mimetype = $config->get('mimetype');
        if ($mimetype) {
            // For local reference
            $options['mimetype'] = $mimetype;
            // For external reference
            $options['ContentType'] = $mimetype;
        }
        
        foreach (static::$metaOptions as $option) {
            if (! $config->has($option)) {
                continue;
            }
            $options[$option] = $config->get($option);
        }
        
        return $options;
    }

    /**
     * Normalize the object result array.
     *
     * @param array $response            
     * @param string $path            
     *
     * @return array
     */
    protected function normalizeResponse(array $response, $path = null)
    {
        $result = [
            'path' => $path
        ];
        $result = array_merge($result, Util::pathinfo($result['path']));
        
        if (isset($response['putTime'])) {
            $result['timestamp'] = $response['putTime'];
        }
        
        if (substr($result['path'], - 1) === '/') {
            $result['type'] = 'dir';
            $result['path'] = rtrim($result['path'], '/');
            
            return $result;
        }
        return array_merge($result, Util::map($response, static::$resultMap), [
            'type' => 'file'
        ]);
    }
}
